#include <dirent.h>
#include <dlfcn.h>
#include <fcntl.h>
#include <string.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <uuid/uuid.h>

#define NIT_SHORT_NAMES
#include <nit/macros.h>
#include <nit/palloc.h>
#include <nit/list.h>
#include <nit/hset.h>
#include <nit/hmap.h>

#include "uii.h"

#define UII_NEW_FD_FLAGS O_WRONLY | O_CREAT | O_TRUNC
#define UII_NEW_FD_MODE S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH

typedef char Path_str[sizeof('/') + 36];

Uii_expr *
uii_expr_val(void *val)
{
	Uii_expr *expr = palloc(expr);

	pcheck(expr, NULL);
	expr->type = UII_VAL;
	expr->i.val = val;
	return expr;
}

Uii_expr *
uii_expr_var(Uii_vid var)
{
	Uii_expr *expr = palloc(expr);

	pcheck(expr, NULL);
	expr->type = UII_VAR;
	uuid_copy(expr->i.var, var);
	return expr;
}

Uii_expr *
uii_expr_fa(Uii_uid f_id, Uii_elist *args)
{
	Uii_expr *expr = palloc(expr);

	pcheck(expr, NULL);
	expr->type = UII_FA;
	uuid_copy(expr->i.fa.f_id, f_id);
	expr->i.fa.args = args;
	return expr;
}

Uii_elist *
uii_elist_new(Uii_expr *expr, Uii_elist *next)
{
	Uii_elist *list = palloc(list);

	pcheck(list, NULL);;
	list->expr = expr;
	LIST_APP(list, next);
	return list;
}

Uii_elist *
uii_nest(Uii_expr *expr)
{
	return uii_elist_new(expr, NULL);
}

static Uii_elist *
va_app(Uii_elist *end, va_list vl)
{
	return LIST_APP(end, uii_elist_new(va_arg(vl, void *), NULL));
}

Uii_elist *
uii_elist_va(int cnt, ...)
{
	Uii_elist *head;
	Uii_elist *end;
	va_list vl;

	if (cnt < 1)
		return NULL;

	va_start(vl, cnt);
	end = head = uii_elist_new(va_arg(vl, void *), NULL);

	for (cnt -= 1; cnt > 0; --cnt)
		pcheck(end = va_app(end, vl), NULL);

	va_end(vl);
	return head;
}

Uii_cnti *
uii_cnti_new(Uii_cnti *up, Uii_elist *ret, Uii_elist *args)
{
	Nit_hmap *env;
	Uii_cnti *cnti;

	cnti = palloc(cnti);
	pcheck(cnti, NULL);
	env = hmap_new(0);
	pcheck_c(env, NULL, free(cnti));
	cnti->up = up;
	cnti->env = env;
	cnti->ret = ret;
	cnti->args = args;
	return cnti;
}

Uii_cnti *
uii_cnti_expr(Uii_expr *expr)
{
	Uii_elist *list = uii_nest(expr);

	pcheck(list, NULL);
	return uii_cnti_new(NULL, list, NULL);
}

Uii_u *
uii_u_new(const char *dir_name, void *focus)
{
	DIR *dir = opendir(dir_name);
	Uii_u *u;

        pcheck(dir, NULL);
        pcheck(u = palloc(u), NULL);
	u->dirfd = dirfd(dir);
	pcheck_c(u->load = hmap_new(0), NULL, free(u));
	u->stack = NULL;
	u->focus = focus;
	return u;
}

int
uii_u_add(Uii_u *u, Uii_f *f)
{
	return hmap_add(u->load, f->id, sizeof(f->id), f, &u->stack);
}

static void
uii_id_path(Path_str path, uuid_t id)
{
	/* first two chars '/' rest */
	uuid_unparse_lower(id, path + 1);
	path[0] = path[1];
	path[1] = path[2];
	path[2] = '/';
}

static int
new_fd(Uii_u *u, Uii_uid id)
{
        Path_str path;
	int fd;

	uii_id_path(path, id);
	fd = openat(u->dirfd, path, UII_NEW_FD_FLAGS, UII_NEW_FD_MODE);

	/* if we can't open the file, then the directory of the first two chars
	 * might not exist, so create that directory and try again.
	 */
	if (fd < 0) {
		char dir[3] = { path[0], path[1], '\0' };

		if (mkdirat(u->dirfd, dir, UII_NEW_FD_MODE) < 0)
			return -1;

		fd = openat(u->dirfd, path, UII_NEW_FD_FLAGS, UII_NEW_FD_MODE);
	}

	return fd;
}

static int
old_fd(Uii_u *u, Uii_uid id)
{
	Path_str path;

	uii_id_path(path, id);
	return openat(u->dirfd, path, O_RDONLY);
}

static int
check_write(int fd, const void *buf, size_t size)
{
	return write(fd, buf, size) == size;
}

static int
write_expr(int fd, Uii_elist *list)
{
	char type;
	Uii_expr *e;
	static const char end = UII_END;

	foreach (list) {
		e = list->expr;
		type = e->type;

		if (!check_write(fd, &type, sizeof(type)))
			return 0;

		switch (type) {
		case UII_VAR:
			if (!check_write(fd, e->i.var, sizeof(e->i.var)))
				return 0;

			break;
		case UII_FA:
			if (!check_write(fd, e->i.fa.f_id,
					 sizeof(e->i.fa.f_id)) ||
			    !write_expr(fd, e->i.fa.args))
				return 0;

			break;
		}
	}

	return check_write(fd, &end, sizeof(end));
}

int
uii_u_ser(Uii_u *u, Uii_f *f)
{
	char type = f->type;
	int fd = new_fd(u, f->id);
	int ret = 1;
	uint32_t lib_len;
	uint32_t sym_len;

	if (fd < 0)
		return 0;

	switch (type) {
	case UII_E:
	        ret = 0;
	        break;
	case UII_D:
		lib_len = strlen(f->i.d->lib);
		sym_len = strlen(f->i.d->sym);

		if (!check_write(fd, &type,     sizeof(type)) ||
		    !check_write(fd, &lib_len,  sizeof(lib_len)) ||
		    !check_write(fd, &sym_len,  sizeof(sym_len)) ||
		    !check_write(fd, f->i.d->lib, lib_len) ||
		    !check_write(fd, f->i.d->sym, sym_len))
			ret = 0;

	        break;
	case UII_C:
		if (!check_write(fd, &type, sizeof(type)) ||
		    !write_expr(fd, f->i.bod))
			ret = 0;

	        break;
	}

	if (close(fd) != 0)
		ret = 0;

	return ret;
}

static int
check_read(int fd, void *buf, size_t size)
{
	return read(fd, buf, size) == size;
}


Uii_f *
uii_u_deser(Uii_u *u, Uii_uid id)
{
	int fd = old_fd(u, id);
	char type;

	if (fd < 0)
		return NULL;

	if (!check_read(fd, &type, sizeof(type))) {
		close(fd);
		return NULL;
	}

	switch (type) {
	case UII_D:
		return uii_df(id, 0);
	}
}

Uii_f *
uii_u_get(Uii_u *u, Uii_uid id)
{
	/* TODO: load from file! */
	return hmap_get(u->load, id, sizeof(Uii_uid));
}

static void
id_gen(uuid_t des, uuid_t src)
{
	if (src)
		uuid_copy(des, src);
	else
		uuid_generate(des);
}

Uii_f *
uii_ef(Uii_uid id, Uii_bf bf)
{
	Uii_f *f;

	f = palloc(f);
	pcheck(f, NULL);
	id_gen(f->id, id);
	f->type = UII_E;
	f->save = 0;
	f->i.e = bf;
	return f;
}

int
uii_dopen(const char *lib)
{
	return !!dlopen(lib, RTLD_LAZY);
}

void
uii_dclose(const char *lib)
{
	void *handle = dlopen(lib, RTLD_LAZY);

	dlclose(handle);
	dlclose(handle);
}

Uii_f *
uii_hdf(Uii_uid id, void *handle, const char *lib, const char *sym, int save)
{
	Uii_df *df;
	Uii_f *f;
	size_t lib_len = strlen(lib) + 1;
	size_t sym_len = strlen(sym) + 1;

	df = palloc(df);
	pcheck(df, NULL);
	f = palloc(f);
	pcheck_c(f, NULL, free(df));
	*(void **) &df->bf = dlsym(handle, sym); /* POSIX.1-2003 workaround */
	pcheck_c(df->bf, NULL, (free(df), free(f)));
	df->lib = malloc(lib_len);
	pcheck_c(df->lib, NULL, (free(df), free(f)));
	strcpy(df->lib, lib);
	df->sym = malloc(sym_len);
	pcheck_c(df->sym, NULL, (free(df), free(f), free(df->lib)));
	strcpy(df->sym, sym);
	id_gen(f->id, id);
	f->type = UII_D;
	f->save = save;
	f->i.d = df;
	return f;
}

Uii_f *
uii_df(Uii_uid id, const char *lib, const char *sym, int save)
{
	void *handle = dlopen(lib, RTLD_LAZY);
	Uii_f *f;

	pcheck(handle, NULL);
	f = uii_hdf(id, handle, lib, sym, save);
	dlclose(handle);
	return f;
}

Uii_f *
uii_cf(Uii_uid id, Uii_elist *bod, int save)
{
	Uii_f *f = palloc(f);

	pcheck(f, NULL);
	id_gen(f->id, id);
	f->type = UII_C;
	f->save = save;
	f->i.bod = bod;
	return f;
}

void
uii_free(Uii_f *f)
{
	switch (f->type) {
	case UII_E:
		break;
	case UII_D:
		free(f->i.d->lib);
		free(f->i.d->sym);
		free(f->i.d);
		break;
	}

	free(f);
}

static void *
var_get(Uii_cnti *cnti, Uii_expr *expr)
{
	void *val = NULL;

	/* dynamic scope */
	for (; !val && cnti; cnti = cnti->up)
		val = hmap_get(cnti->env, expr->i.var, sizeof(expr->i.var));

	return val;
}

static int
cnti_up(Uii_cnti **cnti)
{
	return !!(*cnti = (*cnti)->up);
	/* TODO: do some freeing */
}

static int
cnti_down(Uii_cnti **cnti, Uii_elist *bod, Uii_expr *expr)
{
	Uii_cnti *new = uii_cnti_new(*cnti, bod, expr->i.fa.args);

	pcheck(new, 0);
	*cnti = new;
	return 1;
}

int
uii_eval(Uii_cnti *cnti, Uii_u *u)
{
	Uii_expr *expr;
	Uii_f *f;

	while (1) {
		if (!cnti->ret) {
			if (!cnti_up(&cnti))
				break;

			cnti->ret = LIST_NEXT(cnti->ret, void);
			continue;
		}

		expr = cnti->ret->expr;

		switch (expr->type) {
		case UII_VAL:
			u->focus = expr->i.val;
			cnti->ret = LIST_NEXT(cnti->ret, void);
			continue;
		case UII_VAR:
			u->focus = var_get(cnti, expr);
			cnti->ret = LIST_NEXT(cnti->ret, void);
		        continue;
		case UII_FA:
			f = uii_u_get(u, expr->i.fa.f_id);

			switch (f->type) {
			case UII_E:
				if (!cnti_down(&cnti, NULL, expr))
					return 0;

				f->i.e(cnti, u);
			        continue;
			case UII_D:
				if (!cnti_down(&cnti, NULL, expr))
					return 0;

				f->i.d->bf(cnti, u);
			        continue;
			case UII_C:
				if (!cnti_down(&cnti, f->i.bod, expr))
					return 0;

				continue;
			}
		}
	}

	return 1;
}
